﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using HtmlAgilityPack;
using SCResources.General;

namespace SCResources.ImdbSerieService
{
    public class ImdbServices
    {
        const string BaseUrl = "http://www.imdb.com/";

        #region Patterns
        const string IMDB_ID_REGEX = @"(?<=\w\w)\d{7}";
        const string MOVIE_TITLE_YEAR_PATTERN = @"(.*)\((\d{4}).*"; //(\w+)\s.(\d+).

        const string USER_RATING_PATTERN = @"\b(?<score>\d\.\d)/10"; //"\b\d\.\d/10";
        const string USER_VOTES_PATTERN = @"\d+\,\d+";
        const string FILM_RATING_PATTERN = @"(?<=Rated\s).*(?=\sfor)";

        const string RELEASE_DATE_PATTERN = @"\d+\s\w+\s\d{4}\s";

        const string PERSON_ID = @"(?<=nm)\d+";
        #endregion

        struct SearchState
        {
            public string Code;
            public string CacheKey;
            public bool FullInfo;
        }

        /// <summary>
        /// Match a regexp against a string
        /// </summary>
        /// <param name="s">The string</param>
        /// <param name="pattern">Regex pattern</param>
        /// <returns></returns>
        private Match Match(string s, string pattern)
        {
            return Regex.Match(s, pattern, RegexOptions.IgnoreCase);
        }

        private MatchCollection MatchMany(string s, string pattern)
        {
            return Regex.Matches(s, pattern, RegexOptions.IgnoreCase);
        }

        #region Request_Funcs

        private WebClient CreateWebClient()
        {
            var Wc = new WebClient();
            Wc.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate");
            Wc.Headers.Add(HttpRequestHeader.UserAgent, "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 3.5;)");
            //Wc.Proxy = Proxy
            return Wc;
        }

        private string UncompressResponse(byte[] Response, WebHeaderCollection ResponseHeaders)
        {
            string Html = "";
            using (MemoryStream Ms = new MemoryStream(Response))
            {

                switch (ResponseHeaders[HttpResponseHeader.ContentEncoding])
                {
                    case "gzip":
                        Html = new StreamReader(new GZipStream(Ms, CompressionMode.Decompress)).ReadToEnd();
                        break;
                    case "deflate":
                        Html = new StreamReader(new DeflateStream(Ms, CompressionMode.Decompress)).ReadToEnd();
                        break;
                    default:
                        Html = new StreamReader(Ms).ReadToEnd();
                        break;
                }
            }
            return Html;
        }

        #endregion

        #region FindSerie

        public delegate void FoundSeriesEventHandler(SeriesResultset M);
        /// <summary>
        /// Event fired by FindSeries method
        /// </summary>
        public event FoundSeriesEventHandler FoundSeries;

        public void FindSeries(string movieTitle)
        {
            string Url = BaseUrl + "find?s=all&q=" + movieTitle.UrlEncode();
            
            var Wr = WebRequest.Create(Url);
            Wr.Timeout = 15000;//15 secs
            Wr.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate");
            //Wr.Headers.Add(HttpRequestHeader.UserAgent, "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 3.5;)");
            //Wr.Proxy = Proxy;

            //WebResponse Response = Wr.GetResponse();

            Wr.BeginGetResponse(new AsyncCallback(ResponseCallback), new RequestState() { Request = Wr, MovieTitle = movieTitle });
        }

        void ResponseCallback(IAsyncResult result)
        {
            SeriesResultset R = new SeriesResultset();
            try
            {
                RequestState state = (RequestState)result.AsyncState;
                WebRequest request = (WebRequest)state.Request;
                // get the Response
                HttpWebResponse Response = (HttpWebResponse)request.EndGetResponse(result);
                // process the response...                
                string Html = "";
                var haveBeenRedirected = Match(Response.ResponseUri.ToString(), IMDB_ID_REGEX);

                if (haveBeenRedirected.Success)
                {
                    R.ExactMatches = new List<Serie>();
                    R.ExactMatches.Add(new Serie() { Id = haveBeenRedirected.ToString(), Title = state.MovieTitle.CapitalizeAll() });
                    OnFoundSeries(R);
                }
                else
                {
                    switch (Response.Headers[HttpResponseHeader.ContentEncoding])
                    {
                        case "gzip":
                            Html = new StreamReader(new GZipStream(Response.GetResponseStream(), CompressionMode.Decompress)).ReadToEnd();
                            break;
                        case "deflate":
                            Html = new StreamReader(new DeflateStream(Response.GetResponseStream(), CompressionMode.Decompress)).ReadToEnd();
                            break;
                        default:
                            Html = new StreamReader(Response.GetResponseStream()).ReadToEnd();
                            break;
                    }

                    var H = new HtmlDocument();
                    H.LoadHtml(Html);

                    //Popular titles
                    R.PopularTitles = GetTitles(H, "Popular Titles");
                    R.ExactMatches = GetTitles(H, "Titles (Exact Matches)");
                    R.PartialMatches = GetTitles(H, "Titles (Partial Matches)");

                }            
            }
            catch (Exception ex)
            {
                R.Error = true;
                R.ErrorMessage = ex.Message;
            }
            
            OnFoundSeries(R);
        }

        private List<Serie> GetTitles(HtmlDocument h, string listTitle)
        {
            var List = new List<Serie>();            
            var Nodes = h.DocumentNode.SelectNodes("id('main')//p/b[contains(.,'" + listTitle + "')]/following-sibling::*[1]/tr/td[3]");
            if (Nodes != null)
            {
                try
                {
                    HtmlNode a;
                    foreach (var N in Nodes)
                    {
                        a = N.SelectSingleNode("a");
                        List.Add(new Serie()
                        {
                            Id = Match(a.OuterHtml, IMDB_ID_REGEX).ToString(),
                            Title = a.InnerText.HtmlDecode()
                        });
                    }
                    return List;
                }
                catch {}                
            }
            return null;
        }

        protected virtual void OnFoundSeries(SeriesResultset R)
        {
            ProcessDelegate(FoundSeries, new object[] { R });
        }

        #endregion

        #region GetSerie

        public delegate void MovieParsedEventHandler(Serie M);
        /// <summary>
        /// Event fired by GetMovie method
        /// </summary>
        public event MovieParsedEventHandler MovieParsed;

        #region Own Methodes ---------------------------------------------------------------------

        /// <summary>
        /// Get Serie Cover pic
        /// </summary>
        /// <param name="code"></param>
        /// <param name="fullInfo"></param>
        /// <returns></returns>
        public Image GetSerieCover(string code)
        {
            string Url = BaseUrl + "title/tt" + code;

            string Html = "";

            using (WebClient Wc = CreateWebClient())
            {
                byte[] Data = Wc.DownloadData(new Uri(Url));
                Html = UncompressResponse(Data, Wc.ResponseHeaders);

                HtmlDocument H = new HtmlDocument();
                H.LoadHtml(Html);
                
                return DownloadCover(GetImageUrl(H));
            }
        }

        public delegate void GetSerieCoverAsyncResult(Image result);

        public void GetSerieCoverAsyn(string code, GetSerieCoverAsyncResult getResult)
        {
            Thread getCover = new Thread(delegate (object unused)
                                             {
                                                 getResult(GetSerieCover(code));
                                             });
            getCover.Name = "GetCoverAsync";
            getCover.IsBackground = true;
            getCover.Start();

        }

        private static Image DownloadCover(string coverUrl)
        {
            // Create web client simulating IE6.
            using (WebClient client = new WebClient())
            {
                client.Headers["User-Agent"] = "Mozilla/4.0 (Compatible; Windows NT 5.1; MSIE 6.0)" +
                                               " (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)";

                // Download data.
                byte[] arr = client.DownloadData(coverUrl);

                MemoryStream picStream = new MemoryStream(arr);

                return Image.FromStream(picStream);
            }
        }

        public List<DateTime> GetSerieAirdates(string code, int season)
        {
            string Url = BaseUrl + "title/tt" + code + "/episodes?season="+season;
            
            string Html = "";

            using (WebClient Wc = CreateWebClient())
            {
                byte[] Data = Wc.DownloadData(new Uri(Url));
                Html = UncompressResponse(Data, Wc.ResponseHeaders);

                HtmlDocument H = new HtmlDocument();
                H.LoadHtml(Html);

                return ConverToDateTime(getAirdates(H));
            }
        }

        public DateTime GetSerieAirdate(string code, int season, int episode)
        {
            List<DateTime> dates = GetSerieAirdates(code, season);
            return dates[episode - 1];
        }

        /// <summary>
        /// Get the next airdate for a serie.
        /// </summary>
        /// <param name="code">imdb Id</param>
        /// <param name="season">season, Imdb goes automaticly to latest season if entered season
        /// number is to high.</param>
        /// <returns>SerieAirInfo object, Null if no next airdate is available.</returns>
        public SerieAirInfo GetNextAirdate(string code,int season = -1)
        {
            if(season == -1)
            {
                season = GetLastSeasonNumber(code);
            }

            List<DateTime> dates = GetSerieAirdates(code, season);
            dates.Sort();

            var nextdates = dates.Where(o => o >= DateTime.Now).Select(o => o);

            List<DateTime> nextdatesL = nextdates.ToList();
            nextdatesL.Sort();

            if (nextdatesL.Count != 0)
            {
                SerieAirInfo airInfo = new SerieAirInfo();
                airInfo.Season = season;
                airInfo.Episode = dates.IndexOf(nextdatesL[0]) + 1;
                airInfo.NextAirDate = nextdatesL[0];
                airInfo.Frequency = -1;
            
                return airInfo;
            }

            return null;
        } 

        public int GetLastSeasonNumber(string code)
        {
            string Url = BaseUrl + "title/tt" + code + "/episodes?season="+99999;
            
            string Html = "";

            string temp;

            using (WebClient Wc = CreateWebClient())
            {
                byte[] Data = Wc.DownloadData(new Uri(Url));
                Html = UncompressResponse(Data, Wc.ResponseHeaders);

                HtmlDocument H = new HtmlDocument();
                H.LoadHtml(Html);
                temp = getLastSeason(H);
            }

            int season;
            int.TryParse(Regex.Match(temp, @"(\d{1})").Groups[0].Value, out season);

            return season;

        }

        private enum Months
        {
            Jan = 1,
            Feb = 2,
            Mar = 3,
            Apr = 4,
            May = 5,
            Jun = 6,
            Jul = 7,
            Aug = 8,
            Sep = 9,
            Oct = 10,
            Nov = 11,
            Dec = 12,
        }

        private List<DateTime> ConverToDateTime(List<string> airdates)
        {
            List<DateTime> convertedDates = new List<DateTime>();

            Months months;
            int month = 0;
            int year;
            int day;

            foreach (string airdate in airdates)
            {
                string tempAirdate = airdate;
                //get month
                foreach (string m in Enum.GetNames(typeof(Months)))
                {
                    if (tempAirdate.Contains(m))
                    {
                        Months temp = (Months) Enum.Parse(typeof (Months),m);
                        month = (int)temp;
                        tempAirdate = tempAirdate.Replace(m, ""); // remove the month
                        break;
                    }
                }
                //get year
                Match tempcoll = Regex.Match(tempAirdate, @"(\d{4})");
                int.TryParse(tempcoll.Groups[0].Value,out year);
                tempAirdate = tempAirdate.Replace(year.ToString(), " ");
                //get date
                int.TryParse(Regex.Match(tempAirdate, @"(\d{2})").Groups[0].Value, out day);
                //if day has only 1 digit
                if(day == 0)
                {
                    int.TryParse(Regex.Match(tempAirdate, @"(\d{1})").Groups[0].Value, out day);
                }
                // add date
                if (month != 0 && year != 0 && day != 0)
                {
                    convertedDates.Add(new DateTime(year,month,day));
                }
                else
                {
                    convertedDates.Add(new DateTime(year,month,15,15,15,15)); //in case the day isn't specified.
                }
            }

            return convertedDates;
        }

        #endregion


        /// <summary>
        /// Get movie info from Imdb.com asynchronously
        /// </summary>
        /// <param name="code"></param>
        /// <param name="fullInfo">Get extra info</param>
        public void GetMovieAsync(string code, bool fullInfo)
        {            
            string Url = BaseUrl + "title/tt" + code;

            if (fullInfo)
                Url += "/combined";
            
            
            var State = new SearchState() { Code = code, CacheKey = code + (fullInfo ? "-f" : ""), FullInfo = fullInfo };
            
            
            using (WebClient Wc = CreateWebClient())
            {
                Wc.DownloadDataCompleted += new DownloadDataCompletedEventHandler(Wc_DownloadDataCompleted);                    
                Wc.DownloadDataAsync(new Uri(Url), State);                    
            }
            
        }

        private void Wc_DownloadDataCompleted(object sender, DownloadDataCompletedEventArgs e)
        {
            string Html = "";            
            WebClient Wc = (WebClient)sender;
            SearchState State = (SearchState)e.UserState;
            if (e.Error == null)
            {
                Html = UncompressResponse(e.Result, Wc.ResponseHeaders);
                ParseMovieHTML(State.Code, State.FullInfo, ref Html, false);
            }
            else
            {
                OnMovieParsed(null);
            }
        }

        private Serie ParseMovieHTML(string code, bool fullInfo, ref string html, bool returnValue)
        {
            var Cult = new System.Globalization.CultureInfo("en-US");
            Serie M = new Serie();
            //Html
            HtmlNode Node;
            HtmlNodeCollection Nodes;
            var H = new HtmlDocument();
            H.LoadHtml(html);
            html = null;

            M.Id = code;

            Nodes = H.DocumentNode.SelectNodes("//html/head/title");

            if (Nodes == null)
            {//If we dont have a title, exit the function
                OnMovieParsed(null);
                return null;
            }

            var _Title = Match(Nodes[0].InnerText, MOVIE_TITLE_YEAR_PATTERN).Groups;//TODO: Optimize

            M.Title = _Title[1].ToString().CleanTitle();
            M.Year = int.Parse(_Title[2].ToString());
            
            M.PosterUrl = GetImageUrl(H);
            M.Genres = GetGenres(H);
            

            //SelectSingleNode was 5 times slower in many cases than SelectNodes (Profiled with Ants Profiler 5)

            //Find div that contains user's vote and score
            Nodes = H.DocumentNode.SelectNodes("id('tn15rating')");

            double Rating = 0;
            double Votes = 0;

            if (Nodes != null)
            {
                Node = Nodes[0];
                double.TryParse(Match(Node.InnerText, USER_RATING_PATTERN).Groups["score"].ToString(),
                         System.Globalization.NumberStyles.Float, Cult, out Rating);

                double.TryParse(Match(Node.InnerText, USER_VOTES_PATTERN).ToString(),
                    System.Globalization.NumberStyles.Number, Cult, out Votes);
            }
            M.UserRating = Rating;
            M.Votes = Votes;

            //MPAA Rating
            Node = H.DocumentNode.SelectSingleNode("id('tn15content')//div[@class='info']/a[@href='/mpaa']/../../div[@class]");
            string Rated = string.Empty;

            if (Node != null)
                Rated = Match(Node.InnerText, FILM_RATING_PATTERN).ToString();

            M.Rated = Rated;


            //Release Date
            Node = H.DocumentNode.SelectSingleNode("id('tn15content')//h5[contains(.,'Release Date:')]/../div[@class]");
            DateTime? ReleaseD = null;
            if (Node != null)
            {
                DateTime R;
                if (DateTime.TryParse(Match(Node.InnerText, RELEASE_DATE_PATTERN).ToString(), out R))
                    ReleaseD = R;
            }
            M.ReleaseDate = ReleaseD;

            Node = H.DocumentNode.SelectSingleNode("id('tn15content')//h5[contains(.,'Tagline:')]/../div[@class]");
            if (Node != null)
                //M.Tagline = Node.InnerText;


            Node = H.DocumentNode.SelectSingleNode("id('tn15content')//h5[contains(.,'Runtime:')]/../div[@class]");
            int Runtime = 0;
            if (Node != null)
                int.TryParse(Match(Node.InnerText, @"\d+").ToString(), out Runtime);

            
            M.Languages = GetLanguages(H);
            
            if (returnValue == true)
                return M;
            else
            {
                OnMovieParsed(M);
                return null;
            }
        }

        protected void OnMovieParsed(Serie M)
        {
            if (MovieParsed != null)
                MovieParsed(M);
        }

        #region ParseFunctions

        private string GetDescription(HtmlDocument H)
        {
            var Nodes = H.DocumentNode.SelectNodes("id('tn15content')//div[@class='info']/h5[contains(.,'Plot:')]/../div[@class][1]");
            
            if (Nodes != null)
            {
                foreach (var N in Nodes[0].SelectNodes("a"))
                    N.Remove();

                return Nodes[0].InnerText.CleanHtml();
            }
            return string.Empty;
        }
        private string GetImageUrl(HtmlDocument H)
        {
            var Nodes = H.DocumentNode.SelectNodes("id('img_primary')//a/img[1]");
            if (Nodes != null)
                return Nodes[0].Attributes["src"].Value;
            return string.Empty;
        }
        
        private List<string> GetGenres(HtmlDocument H)
        {
            var Nodes = H.DocumentNode.SelectNodes("id('tn15content')//h5[contains(.,'Genre:')]/../div[@class]/a");
            if (Nodes != null)
                return Nodes.Select(N => N.InnerText).Where(N => N != "more").ToList();                
            return null;
        }
        private List<string> GetLanguages(HtmlDocument H)
        {
            var Nodes = H.DocumentNode.SelectNodes("id('tn15content')//h5[contains(.,'Language:')]/../div[@class]/a");
            if (Nodes != null)
                return Nodes.Select(N => N.InnerText.Trim()).ToList();                
            return null;
        }

        private List<string > getAirdates(HtmlDocument H)
        {
            var List = new List<string>();
            var Node =
                H.DocumentNode.SelectNodes(
                    "id('episodes_content')//div[@class='clear']//div[@class='list detail eplist']//div[@class='list_item odd']//div[@class='info']//div[@class='airdate']");
            
            
            if (Node != null)
                foreach (var N in Node)
                    List.Add(N.InnerText);

            Node =
                H.DocumentNode.SelectNodes(
                    "id('episodes_content')//div[@class='clear']//div[@class='list detail eplist']//div[@class='list_item even']//div[@class='info']//div[@class='airdate']"); 


            if (Node != null)
                foreach (var N in Node)
                    List.Add(N.InnerText);


            return List;
        }

        private string getLastSeason(HtmlDocument H)
        {
            var Node =
               H.DocumentNode.SelectNodes(
               "id('episodes_content')//div[@class='clear']//h3[contains(.,'Season')]");
            return Node[0].InnerHtml;

        }

        private List<int> GetSeasons(HtmlDocument H)
        {
            var List = new List<int>();
            var Node = H.DocumentNode.SelectNodes("id('tn15content')//h5[contains(.,'Seasons:')]/../div[@class]/a");
            int i;
            if (Node != null)
                foreach (var N in Node)
                    if (int.TryParse(N.InnerText, out i))
                        List.Add(i);
            return List;
        }
        private List<string> GetKnownTitles(HtmlDocument H)
        {
            var Node = H.DocumentNode.SelectSingleNode("id('tn15content')//h5[contains(.,'Also Known As:')]/../div[@class]");
            if (Node != null)
            {
                Node.SelectSingleNode("a").Remove();
                return Node.InnerHtml.HtmlDecode().Split(new string[] { "<br>" }, StringSplitOptions.RemoveEmptyEntries).ToList();
            }
            return null;
        }
        private List<Serie> GetRecommendedTitles(HtmlDocument H)
        {
            var Nodes = H.DocumentNode.SelectNodes("id('tn15content')//table[@class='recs']/tbody/tr[2]/td/a");
            if (Nodes != null)
                return Nodes.Select(N => new Serie(){ Id = Match(N.OuterHtml, IMDB_ID_REGEX).ToString(),Title = N.InnerText }).ToList();
            return null;
        }
        private List<string> GetCompanies(HtmlDocument H, string sTitle)
        {
            var Nodes = H.DocumentNode.SelectNodes("id('tn15content')//b[@class='blackcatheader'][contains(.,'" + sTitle + "')]/following-sibling::*[1]/li/a");
            if (Nodes != null)
                return Nodes.Select(N => N.InnerText.HtmlDecode()).Distinct().ToList();
            return null;
        }
        private List<Person> GetPersons(HtmlDocument H, string sTitle)
        {
            var List = new List<Person>();
            var Nodes = H.DocumentNode.SelectNodes("id('tn15content')//h5[contains(.,'" + sTitle + "')]/../../../tr/td[1]");
            if (Nodes != null)
            {
                //Remove the first and the last
                Nodes.RemoveAt(0);
                Nodes.RemoveAt(Nodes.Count - 1);

                return Nodes.Select(N => new Person() { Id = Match(N.InnerHtml, IMDB_ID_REGEX).ToString(), Name = N.InnerHtml.StripHTML() }).ToList();

            }
            return List;
        }

        #endregion

        #endregion

        //Borrowed from http://weblogs.asp.net/rosherove/articles/BackgroundWorker.aspx
        private void ProcessDelegate(Delegate del, params object[] args)
        {
            Delegate temp = del;
            if (temp == null)
            {
                return;
            }
            Delegate[] delegates = temp.GetInvocationList();
            foreach (Delegate handler in delegates)
                InvokeDelegate(handler, args);
        }
        private void InvokeDelegate(Delegate del, object[] args)
        {
            System.ComponentModel.ISynchronizeInvoke synchronizer;
            synchronizer = del.Target as System.ComponentModel.ISynchronizeInvoke;
            if (synchronizer != null) //A Windows Forms object
            {
                if (synchronizer.InvokeRequired == false)
                {
                    del.DynamicInvoke(args);
                    return;
                }
                try
                {
                    synchronizer.Invoke(del, args);
                }
                catch(Exception e)
                {
                    System.Diagnostics.Debug.WriteLine(e.Message);
                    throw e;
                }
            }
            else //Not a Windows Forms object
            {
                del.DynamicInvoke(args);
            }
        }
    }

    public class RequestState
    {
        public WebRequest Request;
        public string MovieTitle;
        public RequestState()
        {
            Request = null;
            MovieTitle = string.Empty;
        }
    }

}
